/* *****************************************************************************
 * JFab (http://code.google.com/p/jfab)
 * Copyright (c) 2011 JFab.org
 * Admin jfab@jeffreythompson.net
 *
 * See the file "LICENSE.txt" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 * *****************************************************************************/

package org.jfab.core.builder;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

import org.jfab.core.collection.CGAnnotationList;
import org.jfab.core.collection.CGConstructorList;
import org.jfab.core.collection.CGFieldList;
import org.jfab.core.collection.CGInitializationBlockList;
import org.jfab.core.collection.CGMethodList;
import org.jfab.core.collection.CGParameterList;
import org.jfab.core.collection.CGTypeList;
import org.jfab.core.collection.CommentList;
import org.jfab.core.collection.MethodBodyList;
import org.jfab.core.collection.ModifierList;
import org.jfab.core.collection.PropertyMapList;
import org.jfab.core.factory.CGClassFactory;
import org.jfab.core.factory.CGMethodFactory;
import org.jfab.core.factory.CGParameterFactory;
import org.jfab.core.factory.CGTypeFactory;
import org.jfab.core.factory.ClassAnalyzer;
import org.jfab.core.factory.TypeReferenceSupport;
import org.jfab.core.model.CGClass;
import org.jfab.core.model.CGMethod;
import org.jfab.core.model.CGParameter;
import org.jfab.core.model.CGType;
import org.jfab.core.model.CGTypeUtilities;
import org.jfab.core.model.Converter;
import org.jfab.core.model.Modifier;

/**
 * Provides a converter class builder.
 */
public final class ConverterCGClassBuilder implements CGClassBuilder<Converter>
{
    /** Number type. */
    private static final Class<Number> NUMBER_TYPE = Number.class;

    /** Class factory. */
    private final CGClassFactory _classFactory;

    /** Convert method types. */
    private final Map<CGType, Map<CGType, CGMethod>> _convertMethodTypes = new HashMap<CGType, Map<CGType, CGMethod>>();

    /** Method comparator. */
    private final Comparator<CGMethod> _methodComparator;

    /** Method factory. */
    private final CGMethodFactory _methodFactory;

    /** Parameter factory. */
    private final CGParameterFactory _parameterFactory;

    /** Type factory. */
    private final CGTypeFactory _typeFactory;

    /** Type reference support. */
    private final TypeReferenceSupport _typeReferenceSupport;

    /** Code generator type utilities. */
    private final CGTypeUtilities _cgTypeUtils;

    /**
     * Construct this object with the given parameters.
     * 
     * @param cgTypeUtils Code generator type utilities. (required)
     * @param typeFactory Type factory.
     * @param typeReferenceSupport Type reference support.
     * @param parameterFactory Parameter factory.
     * @param methodFactory Method factory.
     * @param methodComparator Method comparator.
     * @param classFactory Class factory.
     */
    public ConverterCGClassBuilder(final CGTypeUtilities cgTypeUtils,
            final CGTypeFactory typeFactory,
            final TypeReferenceSupport typeReferenceSupport,
            final CGParameterFactory parameterFactory,
            final CGMethodFactory methodFactory,
            final Comparator<CGMethod> methodComparator,
            final CGClassFactory classFactory)
    {
        _cgTypeUtils = cgTypeUtils;
        _typeFactory = typeFactory;
        _typeReferenceSupport = typeReferenceSupport;
        _parameterFactory = parameterFactory;
        _methodFactory = methodFactory;
        _methodComparator = methodComparator;
        _classFactory = classFactory;
    }

    @Override
    public final CGClass build(final Converter cgConverter)
    {
        final CGType type0 = cgConverter.getType0();
        final CGType type1 = cgConverter.getType1();

        final PropertyMapList propertyMaps = new PropertyMapList(
                cgConverter.getPropertyMaps());

        final CGTypeList referencedTypes = null;
        final String packageName = cgConverter.getPackageName();
        final CommentList commentLines = new CommentList();
        commentLines.add("Provides a converter.");

        final CGAnnotationList annotations = null;
        final ModifierList modifiers = new ModifierList(Modifier.PUBLIC);
        final String className = cgConverter.getName();
        final List<String> typeParameters = null;
        final CGType extendsType = null;
        final CGTypeList implementsInterfaces = null;
        final CGInitializationBlockList initializationBlocks = null;
        final CGConstructorList constructors = null;
        final CGFieldList fields = null;
        final CGMethodList methods = createConvertMethods(type0, type1,
                propertyMaps);

        return getClassFactory().createClass(referencedTypes, packageName,
                commentLines, annotations, modifiers, className,
                typeParameters, extendsType, implementsInterfaces,
                initializationBlocks, constructors, fields, methods);
    }

    /**
     * @param type0 Source type.
     * @param type1 Target type.
     * @param collectionMap Map of target method calls to source accessor.
     */
    private void associateCollectionMethods(final CGType type0,
            final CGType type1, final Map<String, Method> collectionMap)
    {
        // Match type0 accessors to type1 collection accessors.
        final Class<?> class0 = _cgTypeUtils.getType(type0).getRawClass();
        final Class<?> class1 = _cgTypeUtils.getType(type1).getRawClass();

        final ClassAnalyzer classAnalyzer0 = ClassAnalyzer.getInstance(class0);
        final ClassAnalyzer classAnalyzer1 = ClassAnalyzer.getInstance(class1);

        final Set<String> propertyNames0 = new TreeSet<String>();
        propertyNames0.addAll(classAnalyzer0.getAccessiblePropertyNames());

        final Set<String> propertyNames1 = new TreeSet<String>();
        propertyNames1.addAll(classAnalyzer1.getAccessiblePropertyNames());

        final Iterator<String> iter = propertyNames1.iterator();

        while (iter.hasNext())
        {
            final String propertyName = iter.next();
            final Method accessor0 = classAnalyzer0
                    .getAccessorMethodFor(propertyName);

            if (accessor0 != null)
            {
                final Method accessor1 = classAnalyzer1
                        .getAccessorMethodFor(propertyName);

                if (accessor1 != null)
                {
                    final boolean isCollection0 = List.class
                            .isAssignableFrom(accessor0.getReturnType());
                    final boolean isCollection1 = List.class
                            .isAssignableFrom(accessor1.getReturnType());

                    if (isCollection0 && isCollection1)
                    {
                        collectionMap.put(accessor1.getName() + "().addAll",
                                accessor0);
                        propertyNames0.remove(propertyName);
                        iter.remove();
                    }
                }
            }
        }
    }

    /**
     * @param methodMap Method map.
     * @param propertyNames0 Property names.
     * @param propertyNames1 Property names.
     * @param classAnalyzer0 Property names.
     * @param classAnalyzer1 Class analyzer.
     * @param propertyName0 Property name.
     * @param propertyName1 Property name.
     */
    private void associateMappedProperties(final Map<Method, Method> methodMap,
            final Set<String> propertyNames0, final Set<String> propertyNames1,
            final ClassAnalyzer classAnalyzer0,
            final ClassAnalyzer classAnalyzer1, final String propertyName0,
            final String propertyName1)
    {
        final Method accessor = classAnalyzer0
                .getAccessorMethodFor(propertyName0);

        if (accessor == null)
        {
            System.out.println("no accessor found for " + propertyName0
                    + " in " + classAnalyzer0.getSubjectClass().getName());
        }

        final Method mutator = classAnalyzer1
                .getMutatorMethodFor(propertyName1);

        if (mutator == null)
        {
            System.out.println("no mutator found for " + propertyName1 + " in "
                    + classAnalyzer1.getSubjectClass().getName());
        }

        if ((accessor != null) && (mutator != null))
        {
            methodMap.put(mutator, accessor);
            propertyNames0.remove(propertyName0);
            propertyNames1.remove(propertyName1);
        }
    }

    /**
     * Associate type1 mutator methods to type0 accessor methods.
     * 
     * @param type0 Source type.
     * @param type1 Target type.
     * @param propertyMaps Input property maps.
     * @param methodMap Map of target mutator to source accessor.
     */
    private void associateMethods(final CGType type0, final CGType type1,
            final PropertyMapList propertyMaps,
            final Map<Method, Method> methodMap)
    {
        final Class<?> class0 = _cgTypeUtils.getType(type0).getRawClass();
        final Class<?> class1 = _cgTypeUtils.getType(type1).getRawClass();

        final ClassAnalyzer classAnalyzer0 = ClassAnalyzer.getInstance(class0);
        final ClassAnalyzer classAnalyzer1 = ClassAnalyzer.getInstance(class1);

        final Set<String> propertyNames0 = new TreeSet<String>();
        propertyNames0.addAll(classAnalyzer0.getAccessiblePropertyNames());

        final Set<String> propertyNames1 = new TreeSet<String>();
        propertyNames1.addAll(classAnalyzer1.getMutablePropertyNames());

        // Use properties defined in the input.
        for (final Converter.PropertyMap propertyMap : propertyMaps)
        {
            final String propertyName0 = propertyMap.getPropertyName0();
            final String propertyName1 = propertyMap.getPropertyName1();
            associateMappedProperties(methodMap, propertyNames0,
                    propertyNames1, classAnalyzer0, classAnalyzer1,
                    propertyName0, propertyName1);
            associateMappedProperties(methodMap, propertyNames1,
                    propertyNames0, classAnalyzer0, classAnalyzer1,
                    propertyName1, propertyName0);
        }

        // Match type0 accessors to type1 mutators.
        final Iterator<String> iter = propertyNames1.iterator();

        while (iter.hasNext())
        {
            final String propertyName = iter.next();
            final Method accessor = classAnalyzer0
                    .getAccessorMethodFor(propertyName);

            if (accessor != null)
            {
                final Method mutator = classAnalyzer1
                        .getMutatorMethodFor(propertyName);

                if (mutator != null)
                {
                    methodMap.put(mutator, accessor);
                    propertyNames0.remove(propertyName);
                    iter.remove();
                }
            }
        }
    }

    /**
     * @param methods Methods.
     * @param type0 Source type.
     * @param type1 Target type.
     * @param propertyMaps Input property maps.
     * 
     * @return a new convert method.
     */
    private CGMethod createConvertMethod(final CGMethodList methods,
            final CGType type0, final CGType type1,
            final PropertyMapList propertyMaps)
    {
        CGMethod answer = null;

        Map<CGType, CGMethod> typeMethodMap = _convertMethodTypes.get(type0);

        if ((typeMethodMap == null) || (typeMethodMap.get(type1) == null))
        {
            final Class<?> rawClass0 = _cgTypeUtils.getType(type0)
                    .getRawClass();
            final Class<?> rawClass1 = _cgTypeUtils.getType(type1)
                    .getRawClass();

            if (NUMBER_TYPE.isAssignableFrom(rawClass0)
                    && NUMBER_TYPE.isAssignableFrom(rawClass1))
            {
                answer = createConvertNumberMethod(methods, type0, type1,
                        propertyMaps);
            }
            else
            {
                answer = createConvertObjectMethod(methods, type0, type1,
                        propertyMaps);
            }

            if (typeMethodMap == null)
            {
                typeMethodMap = new HashMap<CGType, CGMethod>();
            }

            typeMethodMap.put(type1, answer);
            _convertMethodTypes.put(type0, typeMethodMap);
        }
        else
        {
            answer = typeMethodMap.get(type1);
        }

        return answer;
    }

    /**
     * @param type0 Source type.
     * @param type1 Target type.
     * 
     * @return a new method name.
     */
    private String createConvertMethodName(final CGType type0,
            final CGType type1)
    {
        final String typeName0 = createTypeString(type0);
        final String typeName1 = createTypeString(type1);

        return "convert" + typeName0 + "To" + typeName1;
    }

    /**
     * @param type0 Source type.
     * @param type1 Target type.
     * @param propertyMaps Input property maps.
     * 
     * @return new convert methods.
     */
    private CGMethodList createConvertMethods(final CGType type0,
            final CGType type1, final PropertyMapList propertyMaps)
    {
        final CGMethodList answer = new CGMethodList();

        createConvertMethod(answer, type0, type1, propertyMaps);
        createConvertMethod(answer, type1, type0, propertyMaps);

        Collections.sort(answer, getMethodComparator());

        return answer;
    }

    /**
     * @param methods Methods.
     * @param type0 Source type.
     * @param type1 Target type.
     * @param propertyMaps Input property maps.
     * 
     * @return a new convert method.
     */
    private CGMethod createConvertNumberMethod(final CGMethodList methods,
            final CGType type0, final CGType type1,
            final PropertyMapList propertyMaps)
    {
        final TypeReferenceSupport typeReferenceSupport = getTypeReferenceSupport();
        final String typeName0 = typeReferenceSupport.createTypeString(type0);
        final String typeName1 = typeReferenceSupport.createTypeString(type1);

        final CGTypeList referencedTypes = new CGTypeList();
        referencedTypes.add(type0);
        referencedTypes.add(type1);

        final CommentList commentLines = new CommentList();
        commentLines.add("Convert <code>" + typeName0 + "</code> to <code>"
                + typeName1 + "</code>.");
        commentLines.add("");
        commentLines.add("@param source  Source object.");
        commentLines.add("");
        commentLines.add("@return  converted object.");

        final CGAnnotationList annotations = null;
        final ModifierList modifiers = new ModifierList(Modifier.PRIVATE);
        final CGType returnType = type1;
        final String methodName = createConvertMethodName(type0, type1);
        final CGParameter parameter0 = getParameterFactory().createParameter(
                type0, "source");
        final CGParameterList parameters = new CGParameterList();
        parameters.add(parameter0);

        final CGTypeList throwsTypes = null;
        final MethodBodyList methodBodyLines = new MethodBodyList();
        methodBodyLines.add("return " + typeName1
                + ".valueOf(source.toString());");

        final CGMethodFactory methodFactory = getMethodFactory();

        final CGMethod cgMethod = methodFactory.createMethod(referencedTypes,
                commentLines, annotations, modifiers, returnType, methodName,
                parameters, throwsTypes, methodBodyLines);
        methods.add(cgMethod);

        return cgMethod;
    }

    /**
     * @param methods Methods.
     * @param type0 Source type.
     * @param type1 Target type.
     * @param propertyMaps Input property maps.
     * 
     * @return a new convert method.
     */
    private CGMethod createConvertObjectMethod(final CGMethodList methods,
            final CGType type0, final CGType type1,
            final PropertyMapList propertyMaps)
    {
        // Map mutator method to accessor method.
        final Map<Method, Method> methodMap = new LinkedHashMap<Method, Method>();
        associateMethods(type0, type1, propertyMaps, methodMap);

        // Map collection accessor to collection accessor.
        final Map<String, Method> collectionMap = new LinkedHashMap<String, Method>();
        associateCollectionMethods(type0, type1, collectionMap);

        final CGMethodFactory methodFactory = getMethodFactory();
        final TypeReferenceSupport typeReferenceSupport = getTypeReferenceSupport();
        typeReferenceSupport.addReferencedType(type0);
        typeReferenceSupport.addReferencedType(type1);

        final CGTypeList referencedTypes = new CGTypeList();
        referencedTypes.add(type0);
        referencedTypes.add(type1);

        final String typeName0 = typeReferenceSupport.createTypeString(type0);
        final String typeName1 = typeReferenceSupport.createTypeString(type1);

        final CommentList commentLines = new CommentList();
        commentLines.add("Convert <code>" + typeName0 + "</code> to <code>"
                + typeName1 + "</code>.");
        commentLines.add("");
        commentLines.add("@param source  Source object.");

        final CGParameter parameter0 = getParameterFactory().createParameter(
                type0, "source");
        final CGParameterList parameters = new CGParameterList();
        parameters.add(parameter0);

        final String methodName = createConvertMethodName(type0, type1);

        commentLines.add("@param target  Target object.");

        final CGAnnotationList annotations = null;
        final ModifierList modifiers = new ModifierList(Modifier.PUBLIC);
        final CGType returnType = null;

        final CGParameter parameter1 = getParameterFactory().createParameter(
                type1, "target");
        parameters.add(parameter1);

        final CGTypeList throwsTypes = null;
        final MethodBodyList methodBodyLines = new MethodBodyList();
        methodBodyLines.add("if (source != null && target != null)");
        methodBodyLines.add("{");

        final CGTypeFactory typeFactory = getTypeFactory();

        for (final Entry<Method, Method> entry : methodMap.entrySet())
        {
            final Method mutator = entry.getKey();
            final CGType mutatorParamType = typeFactory.createType(mutator
                    .getParameterTypes()[0]);
            typeReferenceSupport.addReferencedType(mutatorParamType);

            final Method accessor = entry.getValue();
            final CGType accessorReturnType = typeFactory.createType(accessor
                    .getReturnType());
            typeReferenceSupport.addReferencedType(accessorReturnType);

            if (mutatorParamType.equals(accessorReturnType))
            {
                methodBodyLines.add("target." + mutator.getName() + "(source."
                        + accessor.getName() + "());");
            }
            else
            {
                final String typeName20 = createTypeString(accessorReturnType);
                final String typeName21 = createTypeString(mutatorParamType);

                final String methodName2 = "convert" + typeName20 + "To"
                        + typeName21;
                methodBodyLines.add("target." + mutator.getName() + "("
                        + methodName2 + "(source." + accessor.getName()
                        + "()));");

                createConvertMethod(methods, accessorReturnType,
                        mutatorParamType, propertyMaps);
            }
        }

        for (final Entry<String, Method> entry : collectionMap.entrySet())
        {
            methodBodyLines.add("target." + entry.getKey() + "(source."
                    + entry.getValue().getName() + "());");
        }

        methodBodyLines.add("}");

        final CGMethod answer = methodFactory.createMethod(referencedTypes,
                commentLines, annotations, modifiers, returnType, methodName,
                parameters, throwsTypes, methodBodyLines);
        methods.add(answer);

        return answer;
    }

    /**
     * @param type Type.
     * 
     * @return a new type string.
     */
    private String createTypeString(final CGType type)
    {
        final TypeReferenceSupport typeReferenceSupport = getTypeReferenceSupport();
        String answer = typeReferenceSupport.createTypeString(type);

        answer = answer.replaceAll("\\<", "");
        answer = answer.replaceAll("\\?", "");
        answer = answer.replaceAll("\\>", "");

        return answer;
    }

    /**
     * @return the classFactory
     */
    private CGClassFactory getClassFactory()
    {
        return _classFactory;
    }

    /**
     * @return the methodComparator
     */
    private Comparator<CGMethod> getMethodComparator()
    {
        return _methodComparator;
    }

    /**
     * @return the methodFactory
     */
    private CGMethodFactory getMethodFactory()
    {
        return _methodFactory;
    }

    /**
     * @return the parameterFactory
     */
    private CGParameterFactory getParameterFactory()
    {
        return _parameterFactory;
    }

    /**
     * @return the typeFactory
     */
    private CGTypeFactory getTypeFactory()
    {
        return _typeFactory;
    }

    /**
     * @return the typeReferenceSupport
     */
    private TypeReferenceSupport getTypeReferenceSupport()
    {
        return _typeReferenceSupport;
    }
}
